/**
 * jQuery Plasma Plug-in, version 0.5
 * Copyright (c) 2011 Daniel MacDonald dpm@projectatomic.com ALL RIGHTS RESERVED
 * 
 * This is free software; you can redistribute it and/or modify 
 * it under the terms of the GNU Lesser General Public License 
 * as published by the Free Software Foundation; either version 
 * 2.1 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * This plug-in creates a table and cycles the background colors
 * of the table cell with an HSV Sine transformation. This plug-in
 * can become processor intensive with too many rows or columns,
 * which could cause a browser to react slowly, freeze, or crash.
 * The shift and blur options seem to have the most noticeable
 * effect on the plasma.
 * 
 */

(function( $ ){

  var settings = {
		// table settings, only read during initialization
    rows: 8, // more rows = more processing cycles
    cols: 8, // more cols = more processing cycles
    spacing: 0, // table's cellspacing attribute
    padding: 0, // table's cellpadding attribute
    border: 0, // table's border attribute
    height: '40px', // css height property for each cell
    width: '40px', // css height property for each cell
    html: '', // each cell can contain some html
    // timer settings
    shift: 0, // any positive integer
    delay: 100, // milliseconds between frame refresh
    overlay_delay: 1000, // overlay animation speed milliseconds
    scroll_delay: 100, // scroll speed milliseconds
    // plasma settings
    plasma_type: 'hsv', // so far, 'hsv' or 'rgb', more to come
    blur: 1, // less than 1 sharpens the plasma, greater than 1 blurs
    r: 1, // red intensity 0-1, applies to plasma_type 'rgb'
    g: 1, // green intensity 0-1, applies to plasma_type 'rgb'
    b: 1, // blue intensity 0-1, applies to plasma_type 'rgb'
    h: 255, // hue multiplier, applies to plasma_type 'hsv'
    s: 1, // saturation intensity 0-1, applies to plasma_type 'hsv'
    v: 1, // value intensity 0-1, applies to plasma_type 'hsv'
    // overlay settings
    overlay: false, // boolean, on or off
    overlay_anim: false, // boolean true = animated, false = static
    overlay_loop: true, // boolean, true = loop, false = play once
    overlay_color: [255, 255, 255], // array of rgb values 0-255
    overlay_bias: [0,0],  // x,y cells to move overlay 
		overlay_data: [] // array for overlay pattern
  };
/*
 * internal variables to track
 * 
 * timeout
 * buffer
 * overlay_buffer
 * overlay_offset
 * overlay_timeout
 * scroll_buffer
 * scroll_timeout
 */ 
  var $this;
 
  var methods = {

/*
 * Initialize plasma, creates table and sets timeout for animation
 * 
 * @this [jQuery object}
 * @param {object} options
 * @return {void}
 */
    init: function(options) {
     
      return this.each(function(){
        $this = $(this);
        if (options) { 
          $.extend(settings, options);
        };
				$this.data('plasma', settings);

				// set up table
        var table = $('<table/>').attr({cellpadding:settings.padding, cellspacing:settings.spacing, border:settings.border});
        for (i = 0; i < settings.rows; i++) {
          var row = $('<tr/>');
          for (j = 0; j < settings.cols; j++) {
            row.append($('<td/>').css({height:settings.height, width:settings.width}).html(settings.html));
          };
          table.append(row);
        };
        $this.append(table);
				
				// set up buffer
				var buffer = [];
				for (y=0; y<settings.rows; y++) {
					var row = [];
					for (x=0; x<settings.cols; x++) {
						row.push(0);
					};
					buffer.push(row);
				};
				$.extend($this.data('plasma'), {buffer: buffer});
				
				// start main loop
				methods.animate();
      });
    },
		
/*
 * The animate method is called every settings.delay milliseconds
 * updates the buffer, checks for overlays, and advances the shift counter
 * 
 * @return {void}
 */
		animate: function(){
      var data = $this.data('plasma');
			
			// update the buffer
      for (y = 0; y < data.rows; y++) {
        for(x = 0; x < data.cols; x++) {
					color = methods.plasma_color(x, y);
					methods.update_buffer(x, y, color);
        };
      };
			
			// start overlays (scrolling or static) if necessary
			if (data.overlay === true) {
        methods.overlay();
			};
			
			methods.update_screen();
      data.shift++;
      $.extend($this.data('plasma'), {timeout: window.setTimeout(methods.animate, data.delay), shift: data.shift});
		},
  
/*
 * Adds color value to buffer coordinates after checking for overlay
 * 
 * @param {integer} x The table column index
 * @param {integer} y The table row index
 * @param {object} color Object with r, g, b properties {float} 0-255
 * @return {void}
 */    
		update_buffer: function(x, y, color){
			var data = $this.data('plasma');
			
			// check for active overlays
			if (data.overlay == true && $.isArray(data.overlay_buffer)) {
				if (typeof data.overlay_buffer[y] !== 'undefined' && typeof data.overlay_buffer[y][x] !== 'undefined') {
					if ($.isArray(data.overlay_buffer[y][x]) && data.overlay_buffer[y][x].length == 3) {
						// it's a multidimensional array specifying a solid color
				    color = {
							r: data.overlay_buffer[y][x][0],
							g: data.overlay_buffer[y][x][1],
							b: data.overlay_buffer[y][x][2]
						};
					} else if (data.overlay_buffer[y][x] == 1) {
				    if ($.isArray(data.overlay_color) && data.overlay_color.length == 3) {
							// overlay_color is a solid r,g,b 
				      color = {
				        r: data.overlay_color[0],
				        g: data.overlay_color[1],
				        b: data.overlay_color[2]
				      };
				    } else if (typeof data.overlay_color == 'number') {
							// overlay_color is a darker(<0)/lighter(>0) adjustment of animation color 
				      color = {
				        r: color.r * data.overlay_color,
				        g: color.g * data.overlay_color,
				        b: color.b * data.overlay_color
				      };
				    };
					};
				};
			}; 
			data.buffer[y][x] = color;
		},
		
/*
 * Puts buffer content on screen and adjusts for settings.[r,g,b]
 * 
 * @return {void} 
 */    
		update_screen: function(){
			var data = $this.data('plasma');
			var r, g, b;

      for(y = 0; y < data.rows; y++) {
        for(x = 0; x < data.cols; x++) {
					// apply r,g,b settings and insure r,g,b values are integers 0-255
		      r = data.buffer[y][x].r > 255 ? 255 : data.buffer[y][x].r < 0 ? 0 : Math.floor(data.buffer[y][x].r);
		      g = data.buffer[y][x].g > 255 ? 255 : data.buffer[y][x].g < 0 ? 0 : Math.floor(data.buffer[y][x].g);
		      b = data.buffer[y][x].b > 255 ? 255 : data.buffer[y][x].b < 0 ? 0 : Math.floor(data.buffer[y][x].b);
		      
		      $this.find('tr').eq(y).find('td').eq(x).css('background-color', 'rgb('+r+','+g+','+b+')');
        };
      };
		},

 
/*
 * Performs Sine wave transformation on a coordinate, cycles HSV colors, and returns color
 * 
 * @param {integer} x The table column index
 * @param {integer} y The table row index
 * @return {object} color Object with r, g, b properties {float} 0-255
 */    
    plasma_color: function(x, y) {
      var data = $this.data('plasma');

      var distance = function(x1, y1, x2, y2){
        return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
      };

			var HSVtoRGB = function(h, s, v){
	      var r,g,b,i,f,p,q,t;
	      while (h < 0) { h += 359; };
	      h %= 359;
	      
	      s = s > 1 ? 1 : s < 0 ? 0 : s;
	      v = v > 1 ? 1 : v < 0 ? 0 : v;
	      
	      if (s == 0) {
	        r = g = b = v;
	      } else {
	        h /= 60;
	        f = h - (i = Math.floor(h));
	        p = v * (1 - s);
	        q = v * (1 - s * f);
	        t = v * (1 - s * (1 - f));
	        switch (i) {
	          case 0: r = v; g = t; b = p; break;
	          case 1: r = q; g = v; b = p; break;
	          case 2: r = p; g = v; b = t; break;
	          case 3: r = p; g = q; b = v; break;
	          case 4: r = t; g = p; b = v; break;
	          case 5: r = v; g = p; b = q; break;
	        };
	      };
	      return {
	        r: r * 255,
	        g: g * 255,
	        b: b * 255
	      };
			};

      switch (data.plasma_type) {
        case 'rgb': // single color
          var value = Math.abs(Math.sin(distance(x + data.shift / data.blur, y, 128, 128) / 8) + Math.sin(distance(x, y, 64, 64) / 8) + Math.sin(distance(x, y, 192, 64) / 7) + Math.sin(distance(x, y + data.shift / 7 / data.blur, 192, 100) / 8 ));
          var color = {
            r: value * 150 * data.r,
            g: value * 150 * data.g,
            b: value * 150 * data.b
          };          
          break;
        default: // hsv conversion
          var value = Math.sin(distance(x + data.shift / data.blur, y, 128, 128) / 8) + Math.sin(distance(x, y, 64, 64) / 8) + Math.sin(distance(x, y, 192, 64) / 7) + Math.sin(distance(x, y + data.shift / 7 / data.blur, 192, 100) / 8);
          var color = HSVtoRGB(value * data.h / data.blur, data.s, data.v);
      };
			
			return color;
    },
		
/*
 * Sets up static overlay_buffer
 * 
 * @return {void}
 */    
		overlay: function(){
			var data = $this.data('plasma');
      
      if (typeof data.overlay_data == 'string' && (typeof data.scroll_timeout == 'undefined' || data.scroll_timeout == -1)) {
        methods.scroll();
      } else if ($.isArray(data.overlay_data) && data.overlay_anim === true && (typeof data.overlay_timeout == 'undefined' || data.overlay_timeout == -1)) {
        methods.overlay_animate();
      } else if ($.isArray(data.overlay_data) && data.overlay_anim === false) {
        // empty and fill the overlay_buffer
        data.overlay_buffer = [];
        for (y = 0; y < data.rows; y++) { // i = row
          var row = [];
          for (x = 0; x < data.cols; x++) {
            var value = 0;
            if (typeof data.overlay_data[y - data.overlay_bias[1]] !== 'undefined' && typeof data.overlay_data[y - data.overlay_bias[1]][x - data.overlay_bias[0]] !== 'undefined') {
              value = data.overlay_data[y - data.overlay_bias[1]][x - data.overlay_bias[0]];
            };
            row[x] = value;
          };
          data.overlay_buffer.push(row);
        };
      };

			$.extend($this.data('plasma'), data);
		},
    
/*
 * Sets up static overlay_buffer
 * 
 * @return {void}
 */    
    overlay_animate: function(){
      var data = $this.data('plasma');
      
      if (typeof data.overlay_offset == 'undefined' || data.overlay_offset == -1) {
        data.overlay_offset = 0;
      };
      
      var frame = $.isArray(data.overlay_data[data.overlay_offset]) ? data.overlay_data[data.overlay_offset] : [];
      
      if (frame.length > 0){
        data.overlay_buffer = [];
        for (y = 0; y < data.rows; y++) { // i = row
          var row = [];
          for (x = 0; x < data.cols; x++) {
            var value = 0;
            if (typeof frame[y-data.overlay_bias[1]] !== 'undefined' && typeof frame[y-data.overlay_bias[1]][x-data.overlay_bias[0]] !== 'undefined') {
              value = frame[y-data.overlay_bias[1]][x-data.overlay_bias[0]];
            };
            row[x] = value;
          };
          data.overlay_buffer.push(row);
        };
      };
      
      data.overlay_offset++;
      
      if (data.overlay_offset == data.overlay_data.length) {
        if (data.overlay_loop === true) {
          data.overlay_offset = 0;
        } else {
          window.clearTimeout(data.overlay_timeout);
          data.overlay_timeout = 0;
          data.overlay_offset = -1;
        };
      }; 

      if (data.overlay_offset !== -1) {
        data.overlay_timeout = window.setTimeout(methods.overlay_animate, data.overlay_delay);
      };
    },	
/*
 * Sets up scroll_buffer and advances scroll every settings.overlay_delay milliseconds
 * 
 * @requires plasma.chars.js
 * @return {void}
 */    
		scroll: function(){
      //console.log('scroll');
			var data = $this.data('plasma');
			
			// build a scroll buffer if it does not exist
			if (typeof data.scroll_buffer == 'undefined' || data.scroll_buffer.length == 0) {
				data.scroll_buffer = []; 
				data.scroll_offset = 0;
				data.overlay_buffer = [];
				
				for (i=0; i<data.rows; i++) { // i = row
					// add spaces to beginning so pattern scrolls onto frame
					var row = [];
					for (j=0; j<data.cols; j++) {
						row.push(0);
					};
					for (j=0, m=data.overlay_data.length; j<m; j++) { // j = character
						var this_index = $.inArray(data.overlay_data[j], plasma_char_map) == -1 ? 0 : $.inArray(data.overlay_data[j], plasma_char_map);
						var this_char = plasma_char_data[this_index];
						if (i < this_char.length) {
							for (k=0, n=this_char[i].length; k<n; k++) { // k = column
							  var value = 0;
                if (typeof this_char[i-data.overlay_bias[1]] !== 'undefined' && typeof this_char[i-data.overlay_bias[1]][k] !== 'undefined') {
                  value = this_char[i-data.overlay_bias[1]][k];
                };
								row.push(value);
							};
						};
					};
					// add spaces to the end so message scrolls off frame
					for (j=0; j<data.cols; j++) {
						row.push(0);
					};
					data.scroll_buffer.push(row);
				};
			};	
			
			if (data.scroll_offset < data.scroll_buffer[0].length - data.cols) {
				// empty and fill the overlay_buffer
				data.overlay_buffer = [];
				for (i=0, l=data.scroll_buffer.length; i<l; i++) { // loop throught rows
					var row = data.scroll_buffer[i].slice(data.scroll_offset, (data.scroll_offset + data.cols));
						data.overlay_buffer.push(row);
				};
				data.scroll_offset++;
			} else {
				// we have scrolled to the end, clean up
        if (data.overlay_loop === true) {
          data.scroll_offset = 0;
        } else {
          window.clearTimeout(data.scroll_timeout);
          data.scroll_timeout = -1;
          data.scroll_offset = -1;
          data.overlay = false;
          data.overlay_buffer = [];
        };
			};
      if (data.overlay === true) {
        data.scroll_timeout = window.setTimeout(methods.scroll, data.scroll_delay);
      };

			$.extend($this.data('plasma'), data);
		},
		
/**
 * jQuery plug-in methods (public)
 * $(selector).plasma('[method_name]', [arguments]);
**/
/*
 * Starts a stopped animation
 * 
 * @return {object} jQuery
 */    
    start : function() {
      return this.each(function(){
        if ($(this).data('plasma')) {
          methods.animate();
        } else {
					$(this).plasma();
				};
      });
    },

/*
 * Clears data.timeout stopping the animation
 * but keeping all variables intact
 * 
 * @return {object} jQuery
 */    
    stop : function() {
      var data =  this.data('plasma');
      return this.each(function(){
        if (data) {
          window.clearTimeout(data.timeout);
          window.clearTimeout(data.overlay_timeout);
          data.overlay_timeout = -1;
          $.extend($(this).data('plasma'), data);
        };
      });
    },

/*
 * Applies an overlay to the animation
 * overlay_data can be an array specifying values for each point on the matrix
 * or a string which will scroll across the matrix
 * 
 * @param {array|string} overlay_data can be an array of colors/values or a string which scrolls across the matrix
 * @return {object} jQuery
 */    
    show_overlay : function(overlay_data) {
			return this.each(function(){
				$.extend($(this).data('plasma'), {overlay: true, overlay_data: overlay_data});
      });
    },

/*
 * Turns off the overlay and resets all variables
 * 
 * @return {object} jQuery
 */    
    hide_overlay : function() {
			return this.each(function(){
        var data = $this.data('plasma');
        if (typeof data.scroll_timeout != 'undefined') {
          window.clearTimeout(data.scroll_timeout);
          data.scroll_timeout = -1;
          data.scroll_offset = -1;
        };
        if (typeof data.overlay_timeout != 'undefined') {
          window.clearTimeout(data.overlay_timeout);
          data.overlay_timeout = -1;
          data.overlay_offset = -1;
          data.overlay_anim = false;
        };
        data.overlay = false;
				data.overlay_buffer = [];
        $.extend($(this).data('plasma'), data);
      });
    },

/*
 * Completely removes the plasma table from the DOM
 * clears timeouts, removes data
 * 
 * @return {object} jQuery
 */    
    destroy : function() {
      return this.each(function(){
        if ($(this).data('plasma')) {
          var data = $this.data('plasma');
          if (typeof data.scroll_timeout != 'undefined') {
            window.clearTimeout(data.scroll_timeout);
            data.scroll_timeout = -1;
            data.scroll_offset = -1;
          };
          if (typeof data.overlay_timeout != 'undefined') {
            window.clearTimeout(data.overlay_timeout);
            data.overlay_timeout = -1;
            data.overlay_offset = -1;
          };
          data.overlay = false;
          data.overlay_buffer = [];
          window.clearTimeout(data.timeout);
          $(this).removeData('plasma').empty();
        };
      });
    }
  };

/**
 * jQuery plug-in declaration
 * 
 * @param {string|object} method name to apply, or init with options object
 * @return {object} jQuery
**/
  $.fn.plasma = function(method) {
    if (methods[method]) {
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    } else if (typeof method === 'object' || !method) {
      return methods.init.apply(this, arguments);
    } else {
      $.error('Method ' + method + ' does not exist on jQuery.plasma');
    };   
  };

})(jQuery);
